package com.guohang.mic_01;

//changes in this version:
//clean up code
//new entropy filter
//new energy / entropy threshold
//window = 10mins
import java.util.Calendar;
import java.util.concurrent.ArrayBlockingQueue;

import android.app.Service;
import android.content.Intent;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder.AudioSource;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.util.Log;


public class ContextService extends Service {
	
    private long mLastTime;
    private final static int REFRESH_PERIOD = 2000;
	public static final int RECORDER_SAMPLERATE = 8000;
    public int position = 0;
	public boolean heard = false;
	
	public final static int FRAME_SIZE = 1024;
	public final static int FEATURE_COUNT = 12;
	
	public int sampleRate;
	private static ArrayBlockingQueue<Float> mFrameBuffer;
	
	private RecordAmplitudeTask recordTask;
	private BuildModelTask modelTask;
	
	//database
	public CommentsDataSource dataSource;
	
	//communication with activity by messenger
	private Messenger mMessenger;
	
    
    @Override
    public void onCreate() {
        super.onCreate();
        
        mFrameBuffer = new ArrayBlockingQueue(FRAME_SIZE);
  
    }
    
    @Override
    public void onDestroy(){
    	modelTask.cancel(true);
    	recordTask.cancel(true);
    	super.onDestroy();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId){
    	
        Bundle extras = intent.getExtras();
        // Get messager from the Activity
        if (extras != null) {
          mMessenger = (Messenger) extras.get("MESSENGER");

        }
    	
        //initialize database connection
        dataSource = new CommentsDataSource(this);
        dataSource.open();
        
       
        //start AsyncTasks for audio recording and model building
        recordTask = new RecordAmplitudeTask();
        recordTask.execute();
        
        modelTask = new BuildModelTask();
        modelTask.execute();
        
    	return START_STICKY;
    };
   
   //Create Audio Recorder Manager Class
    public class AudioClipRecorder{

    	private AudioRecord recorder;
    	
    	private boolean continueRecording;
    	
    	private static final String TAG = "AudioClipRecorder";
    	
    	private static final int DEFAULT_BUFFER_INCREASE_FACTOR = 3;
    	
    	private FrameAdmissionControl frameControl = new FrameAdmissionControl(RECORDER_SAMPLERATE);

    	private AsyncTask task;

        //Constructors	
    	public AudioClipRecorder(AsyncTask task)
    	{
    	this.task = task;
    	}
    	
    	//Buffer stuff starts here.
    	public void startRecording()
    	{
    	startRecording(RECORDER_SAMPLERATE, AudioFormat.ENCODING_PCM_16BIT);
    	}
    	
    	
    	public void startRecording(final int sampleRate, int encoding)
    	{
    	  int bufferSize = determineMinimumBufferSize(sampleRate, encoding);
    	  doRecording(sampleRate, encoding, bufferSize, bufferSize, DEFAULT_BUFFER_INCREASE_FACTOR);
    	}
    	
    	private int determineMinimumBufferSize(final int sampleRate, int encoding)
    	{
    	  int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_IN_MONO, encoding);
    	  return minBufferSize;
    	}
    	
    	private int determineCalculatedBufferSize(final int sampleRate, int encoding, int numSamplesInBuffer)
    			{
    			  int minBufferSize = determineMinimumBufferSize(sampleRate, encoding);
    			  int bufferSize;
    			  // each sample takes two bytes, need a bigger buffer
    			  if (encoding == AudioFormat.ENCODING_PCM_16BIT)
    			    {
    			      bufferSize = numSamplesInBuffer * 2;
    			    }
    			  else
    			    {
    			      bufferSize = numSamplesInBuffer;
    			    }
    			  
    			  if (bufferSize < minBufferSize)
    			  {
    			  Log.w(TAG, "Increasing buffer to hold enough samples " + minBufferSize + " was: " + bufferSize);
    			  bufferSize = minBufferSize;
    			  }
    			  return bufferSize;
    			  }
    	
    	//the real job starts here
    	private void doRecording(final int sampleRate, int encoding, int recordingBufferSize, int readBufferSize, int bufferIncreaseFactor)
    			{
    			if (recordingBufferSize == AudioRecord.ERROR_BAD_VALUE)
    			{
    			Log.e(TAG, "Bad encoding value, see logcat");
    			}
    			else if (recordingBufferSize == AudioRecord.ERROR)
    			{
    			Log.e(TAG, "Error creating buffer size");
    			}
    			// give it extra space to prevent overflow
    			int increasedRecordingBufferSize = recordingBufferSize * bufferIncreaseFactor;
    			//*****Create recorder
    			recorder = new AudioRecord(AudioSource.MIC, sampleRate, AudioFormat.CHANNEL_IN_MONO, encoding, increasedRecordingBufferSize);
    			
    			//*****Allocate read buffer space
    			final short[] readBuffer = new short[readBufferSize];
    			
    			Log.d(TAG, "start recording, " + "recording bufferSize: "
    			+ increasedRecordingBufferSize
    			+ " read buffer size: " + readBufferSize);
    			
    			//*****Start recording
    			recorder.startRecording();
    			
    			while (true)
    			{
    				//*****Read from buffer
    				int bufferResult = recorder.read(readBuffer, 0, readBufferSize);
    			//in case external code stopped this while read was happening
    			if (((task == null) && task.isCancelled()))
    			{ 
    				done();
    				break;
    			}
    			// check for error conditions
    			if (bufferResult == AudioRecord.ERROR_INVALID_OPERATION)
    			{
    			Log.e(TAG, "error reading: ERROR_INVALID_OPERATION");
    			}
    			else if (bufferResult == AudioRecord.ERROR_BAD_VALUE)
    			{
    			Log.e(TAG, "error reading: ERROR_BAD_VALUE");
    			}
    			else
    			// no errors, do processing
    			{
    				frameControl.bufferListener(readBuffer);
    			}
    			}
    			
    			done();

    			}
    	
    	
    	public void done()
    	{
    	  Log.d(TAG, "shut down recorder");
    	  if (recorder != null)
    	   {
    	     recorder.stop();
    	     recorder.release();
    	     recorder = null;
    	}
    	}
    }
    

     
    public class FrameAdmissionControl{
    	//logics to decide if a frame should go to further processes
    	
        private boolean LAST_FRAME_RESULT = false;
        private final int CRUISE_INTERVAL = 5000;
        private final int MFCC_FREE_INTERVAL = 2000;
        private long mLastCheck; //in eventless mood, last time to check frame
        private long mLastMFCC;
    	
        private int sampleRate;
        
        public FrameAdmissionControl(int sampleRate){

        	this.sampleRate = sampleRate;
        }
        
        public void bufferListener(short[] data){
        	//convert from short to float
            float[] mfccFrame = new float[FRAME_SIZE];
        	
        	for(int i=0; i<data.length; i++){
        		mfccFrame[i] = (float)data[i];
        	}
        	
        	//if last frame is good, pass next 2s frame without checking for energy/entropy
        	if(LAST_FRAME_RESULT){
        		//1. pass next 2s frames without checking
        		//2. check entropy of incoming frame
        		if(!checkTime(mLastMFCC,MFCC_FREE_INTERVAL)){
        			//do MFCC directly
        			addToBuffer(mfccFrame);
                    
        		}else{
        			if(entropyCheck(mfccFrame)){
        				//pass to do MFCC
        				addToBuffer(mfccFrame);
        				LAST_FRAME_RESULT = true;
        			}else{
        				LAST_FRAME_RESULT = false;
        				return;
        				}
        		}
        	}else{
        		//check time if time since last check > 5s, do entropy check, otherwise return
        		if(checkTime(mLastCheck, CRUISE_INTERVAL)){
            	    mLastCheck = Calendar.getInstance().getTimeInMillis(); 
        			if(entropyCheck(mfccFrame)){
        				//pass to do MFCC
        				addToBuffer(mfccFrame);
        				LAST_FRAME_RESULT = true;
        			}else{
        				LAST_FRAME_RESULT = false;
        				return;
        				}
        		}else{return;}
        	}
        }
        
        private void addToBuffer(float[] data){
        	
        	//wait the other thread to take value from the queue.
        	while(mFrameBuffer.size() > 3500){
        		try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
        	}
 		   
          for(int i = 0; i < data.length; i++){	
        	try{
			   mFrameBuffer.add(data[i]);
		       } catch (IllegalStateException e){
			   ArrayBlockingQueue<Float> newBuf = new ArrayBlockingQueue<Float>(mFrameBuffer.size()*2);
			   mFrameBuffer.drainTo(newBuf);
			   mFrameBuffer = newBuf;
			   mFrameBuffer.add(data[i]);
		       }	
          }
        }
        
        private boolean checkTime(long lastTime, int interval )
        {
            // how much time has passed since the last update?
            long currTime = Calendar.getInstance().getTimeInMillis();
            long dt = currTime - lastTime;
            
            if (dt > interval )
    	    {
            	return true; 
    	    }else{return false;}
        }
       
        private boolean entropyCheck(float[] data){
        	boolean returnValue = false;
        	float energyThreshold = (float)0.03; // a dummy value
        	float entropyThreshold = 0;
        	double sum = 0;
        	float energy = 0;
        	double entropy = 0;
        	
        	//update MFCC timer 
        	mLastMFCC = Calendar.getInstance().getTimeInMillis(); 
        	
        	//Energy Check and build a double array 
        	double[] entropyArray = new double[data.length];
        	
        	for(int i = 0; i < data.length; i++){
        		entropyArray[i] = data[i];
        		sum += data[i] * data[i]; 	
        	}
        	energy = (float) Math.sqrt(sum / data.length);
        	sum = 0;
        	
        	//Entropy Check
        	
        	double[] ans=new double[data.length];
            for(int k=0;k<entropyArray.length;k++) {
                    ans[k]=Math.abs(entropyArray[k]);
                    sum += ans[k];
            }
            if(sum == 0){ 
            	return returnValue = false; 
            	}
            
            double[] ans01=new double[ans.length];
            for(int m=0; m<ans.length;m++){
            	ans01[m] = ans[m] / sum;
            	if(ans01[m] != 0){ 
            	  entropy -= ans01[m]*Math.log(ans01[m]);
            	}
            }
        	sum = 0;
           
        	//compare with threshold value
        	if(energy > energyThreshold || entropy < entropyThreshold){
        		returnValue = true;
        		return returnValue;
        	}else{return returnValue = false;}        	

        }
    }
    private class RecordAmplitudeTask extends AsyncTask<Void, Void, Void>
    {


		@Override
		protected Void doInBackground(Void...arg0){
			
			AudioClipRecorder recorder = new AudioClipRecorder(this);
		
			recorder.startRecording();
			return null;
		}
				
    }
    
    private class BuildModelTask extends AsyncTask<Void, Void, Void>{
 	   
        BinManager binManager = new BinManager(mMessenger, getBaseContext());
    	FeatureModel model = new FeatureModel(FRAME_SIZE, RECORDER_SAMPLERATE, FEATURE_COUNT, dataSource, binManager);
      
 	   @Override
 	   protected Void doInBackground(Void...arg0){
 		  float[] accBlock = new float[FRAME_SIZE];
 		  int blockSize = 0;
		  
 		  while(true){
 		    try {
				accBlock[blockSize++] = mFrameBuffer.take().floatValue();
				
				if(blockSize == FRAME_SIZE){
					model.updateModel(accBlock);
					accBlock = new float[FRAME_SIZE];
					blockSize = 0;
					continue;
				}
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
 		  }
       } 
 	   
 	   @Override
 	   protected void onCancelled(){
 		   dataSource.close();
 		   model.taskFinish();
 		   super.onCancelled();
 	   }
 
    }

	@Override
	public IBinder onBind(Intent arg0) {
		// TODO Auto-generated method stub
		return null;
	}
    

}
